package gdma.hqdes.webapp.controller;

import common.annotation.Traced;
import common.exception.BadRequestException;
import common.mybatis.Queries;
import common.util.TreeUtils;
import gdma.hqdes.article.renderer.UnitScoreTableExcel;
import gdma.hqdes.bean.*;
import gdma.hqdes.calc.FormulaCalculator;
import gdma.hqdes.calc.ScoreCalculator;
import gdma.hqdes.service.IndexService;
import gdma.hqdes.service.ProjectService;
import gdma.hqdes.service.UnitService;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import io.vavr.Tuple;
import io.vavr.Tuple3;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RegExUtils;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import sindconsole.auth.bean.User;
import sindconsole.auth.service.AuthService;
import springfox.documentation.annotations.ApiIgnore;

import javax.servlet.http.HttpServletResponse;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Collectors;

import static gdma.hqdes.Constants.*;
import static gdma.hqdes.bean.UnitScore.*;

/**
 * @Author wangyue
 * @Version 3.0
 * @Date 2024-07-14
 */

@RestController
@RequestMapping("/api")
@Slf4j
public class UnitScoreController {

    @Autowired
    AuthService authService;

    @Autowired
    UnitService unitService;

    @Autowired
    IndexService indexService;

    @Autowired
    ProjectService projectService;

    @ApiOperation("获取单位信息")
    @GetMapping("/units/{unitId}")
    @RequiresAuthentication
    public Object unitInfo(@ApiParam("单位id") @PathVariable("unitId") Long unitId) {
        return  unitService.getUnit(unitId);
    }

    @ApiOperation("当前账号所属单位信息")
    @GetMapping("/subject/unit")
    @RequiresAuthentication
    public Object subjectUnit(@ApiIgnore Unit subjectUnit) {
        return subjectUnit;
    }

    @ApiOperation("当前账号所参与的项目")
    @GetMapping("/subject/projects")
    @RequiresAuthentication
    public Object subjectProjects(@ApiIgnore List<HqdProject> currentProjects) {
        return currentProjects;
    }

    @ApiOperation("获取当前账号待上报评分")
    @GetMapping("/subject/to-report/{projectId}")
    @RequiresAuthentication
    public List<UnitStat2> notifyUnitScoresToReport(@PathVariable("projectId") Integer projectId,
                                                   @ApiIgnore User subjectUser,
                                                   @ApiIgnore List<Long> authorizedDomainIds) {
        HqdProject project = projectService.getProject(projectId);
        Set<String> userRoles = authService.findUserRoleKeys(subjectUser.getId());
        return unitService.notifyUnitScoresToReport(project, authorizedDomainIds, userRoles);
    }

    @ApiOperation("获取当前账号待审核评分")
    @GetMapping("/subject/to-audit/{projectId}")
    @RequiresAuthentication
    public List<UnitStat2> notifyUnitScoresToAudit(@PathVariable("projectId") Integer projectId,
                                                  @ApiIgnore User subjectUser,
                                                  @ApiIgnore List<Long> authorizedDomainIds) {
        HqdProject project = projectService.getProject(projectId);
        Set<String> userRoles = authService.findUserRoleKeys(subjectUser.getId());
        return unitService.notifyUnitScoresToAudit(project, authorizedDomainIds, userRoles);
    }

    @Data
    static class ProjectSummary extends HqdProject {
        int toReportCount;
        int toAuditCount;
    }

    @ApiOperation("欢迎页面，显示当前项目进度、个人待办等信息")
    @GetMapping("/welcome")
    @RequiresAuthentication
    public Object welcome(@ApiIgnore List<HqdProject> currentProjects,
                          @ApiIgnore User subjectUser,
                          @ApiIgnore List<Long> authorizedDomainIds,
                          @ApiIgnore Unit subjectUnit) {
        Set<String> userRoles = authService.findUserRoleKeys(subjectUser.getId());
        List<ProjectSummary> result = currentProjects.stream().map(project -> {
            ProjectSummary summary = new ProjectSummary();
            BeanUtils.copyProperties(project, summary);
            List<UnitStat2> stats = unitService.notifyUnitScoresToReport(project, authorizedDomainIds, userRoles);
            Optional<Integer> count = stats.stream()
                    .map(us -> us.getScoreMap().size())
                    .collect(Collectors.reducing((a, b) -> a + b));
            summary.setToReportCount(count.orElse(0));
            stats = unitService.notifyUnitScoresToAudit(project, authorizedDomainIds, userRoles);
            count = stats.stream()
                    .map(us -> us.getScoreMap().size())
                    .collect(Collectors.reducing((a, b) -> a + b));
            summary.setToAuditCount(count.orElse(0));
            return summary;
        }).collect(Collectors.toList());
        return result;
    }

    @ApiOperation("获取最近更新评分列表，按：待办>更新时间 顺序排列")
    @GetMapping("/{year}/scopes/{scope}")
    @RequiresAuthentication
    public Object units(@ApiParam("年份") @PathVariable("year") Integer year,
                        @ApiParam("单位层级：单位层级：2=地市级, 3=县级") @PathVariable("scope") int scope,
                        @ApiParam("单位名称") @RequestParam(value = "name", required = false) String name,
                        @ApiParam("区域标签：Z=珠三角，Y=粤东西北") @RequestParam(value = "areaTag", required = false) String areaTag,
                        @ApiParam("页码，0开始") @RequestParam(value = "page", defaultValue = "0") int page,
                        @ApiParam("页长，默认20") @RequestParam(value = "pageSize", required = false) Integer pageSize,
                        @ApiIgnore User subjectUser,
                        @ApiIgnore List<Long> authorizedDomainIds,
                        @ApiIgnore Unit subjectUnit) {
        HqdProject project = projectService.getProject(year, scope);
        Set<String> userRoles = authService.findUserRoleKeys(subjectUser.getId());
        Map<Long, Integer> notificationCounts = unitService.notifyUnitScoresToReport(project, authorizedDomainIds, userRoles).stream()
                .collect(Collectors.toMap(UnitStat2::getId, us2 -> us2.getScoreMap().size()));
        Map<Long, Integer> notificationCounts2 = unitService.notifyUnitScoresToAudit(project, authorizedDomainIds, userRoles).stream()
                .collect(Collectors.toMap(UnitStat2::getId, us2 -> us2.getScoreMap().size()));
        notificationCounts2.forEach((key, value) -> notificationCounts.merge(key, value, Integer::sum));
        int[] pagination = pageSize != null ? new int[] {page, pageSize} : new int[] {page};
        Page<Unit> result = Queries.paginate(unitService.listRecentUpdatedUnits(project, authorizedDomainIds, name, areaTag, notificationCounts.keySet()), pagination);
        for(Unit unit : result.getContent()) {
            unit.setIsNotifying(notificationCounts.getOrDefault(unit.getId(), 0));
        }
        return result;
    }

    private static void assertAuthorizedUnit(Long unitId, List<Long> authorizedDomainIds) {
        if(authorizedDomainIds != null && !authorizedDomainIds.contains(unitId))
            throw new AuthorizationException("不具有访问该单位数据的权限" );
    }

    @ApiOperation("获取单位的指标树（含当前得分）")
    @GetMapping("/{year}/units/{unitId}/index-scores")
    @RequiresAuthentication
    public Object indexScores(@ApiParam("年份") @PathVariable("year") Integer year,
                              @ApiParam("单位id") @PathVariable("unitId") Long unitId) {
        Unit unit = unitService.getUnit(unitId);
        HqdProject hqdProject = projectService.getProject(year, unit.getScope());
        return indexService.buildScoredIndexTree(hqdProject.getId(), unit.getId());
    }

    @ApiOperation("计算指标项得分（不更新数据）")
    @PostMapping("/{year}/units/{unitId}/index-scores/{indexId}/calc")
    @RequiresAuthentication
    public Object indexScoresCalc(@ApiParam("年份") @PathVariable("year") Integer year,
                                  @ApiParam("单位id") @PathVariable("unitId") Long unitId,
                                  @ApiParam("指标项id") @PathVariable("indexId") long indexId,
                                  @ApiParam("计算参数，字典形式：{'a':1}") @RequestBody Map<String, String> scoreArgs,
                                  @ApiIgnore User subjectUser,
                                  @ApiIgnore List<Long> authorizedDomainIds) {
        //assertAuthorizedUnit(unitId, authorizedDomainIds);
        Unit unit = unitService.getUnit(unitId);
        HqdIndex index = indexService.getIndex(indexId);
        HqdIndexExt indexExt = index.getIndexExt(unit.getAreaTag());
        if(indexExt == null) return null;
        //验证登入人是否具有正确的权限
        //Set<String> userRoles = authService.findUserRoleKeys(subjectUser.getId());
        //assertRoles(userRoles, indexExt.getReportRoles());
        //计算评分
        try {
            float rawScore = calculateScore(index, indexExt, scoreArgs);
            float finalScore = nondimensionalizeScore(rawScore, indexExt.getThresholdScore(), indexExt.getTargetScore());
            Map<String, Float> scores = new HashMap<>();
            scores.put("rawScore", rawScore);
            scores.put("finalScore", finalScore);
            return scores;
        } catch(Exception x) {
            log.warn("计算过程出错，返回0值", x);
            return 0f;
        }
    }

    private static void assertRoles(Set<String> userRoles, String... roles) {
        boolean contained = false;
        for(String role : roles) {
            contained |= userRoles.contains(role);
        }
        if(!contained) throw new AuthorizationException("需要角色：" + String.join("、", roles));
    }

    private static float calculateScore(HqdIndex index, HqdIndexExt indexExt, Map<String, String> scoreArgs) {
        if(StringUtils.hasText(indexExt.getCalcClass())) {
            String calcClassName = indexExt.getCalcClass();
            if(!calcClassName.contains("."))
                calcClassName = FormulaCalculator.class.getPackage().getName() + "." + calcClassName;
            try {
                ScoreCalculator scoreCalculator = (ScoreCalculator)Class.forName(calcClassName).newInstance();
                return scoreCalculator.doCalc(scoreArgs);
            } catch(ClassNotFoundException e) {
                throw new RuntimeException(e);
            } catch(InstantiationException e) {
                throw new RuntimeException(e);
            } catch(IllegalAccessException e) {
                throw new RuntimeException(e);
            }
        }
        if(StringUtils.hasText(indexExt.getFormula())) {
            ScoreCalculator scoreCalculator = new FormulaCalculator(indexExt.getFormula());
            return scoreCalculator.doCalc(scoreArgs);
        }
        throw new RuntimeException("未定义指标的计算方式: " + index);
    }

    private static float nondimensionalizeScore(float score, float thresholdScore, float targetScore) {
        if(thresholdScore > targetScore) {
            //逆向指标
            if(score > thresholdScore) return 0;
            if(score < targetScore) return 100;
            return 100 / (thresholdScore - targetScore) * (thresholdScore - score);
        } else {
            //正逆向指标
            if(score < thresholdScore) return 0;
            if(score > targetScore) return 100;
            return 100 / (targetScore - thresholdScore) * (score - thresholdScore);
        }
    }

    @ApiOperation("更新指标项得分（同时计算相关联的一、二级指标得分并更新）")
    @PostMapping("/{year}/units/{unitId}/index-scores/{indexId}/commit")
    @RequiresRoles(ROLE_REPORTER)
    @Traced
    public Object indexScoresCommit(@ApiParam("年份") @PathVariable("year") Integer year,
                                    @ApiParam("单位id") @PathVariable("unitId") Long unitId,
                                    @ApiParam("指标项id") @PathVariable("indexId") Long indexId,
                                    @ApiParam("计算得分（人工指定时使用）") @RequestParam(value = "rawScore", required = false) Float rawScore,
                                    @ApiParam("最终得分（人工指定时使用）") @RequestParam(value = "finalScore", required = false) Float finalScore,
                                    @ApiParam("计算参数") @RequestBody UnitScore inputScore,
                                    @ApiIgnore User subjectUser,
                                    @ApiIgnore List<Long> authorizedDomainIds) {
        assertAuthorizedUnit(unitId, authorizedDomainIds);
        Unit unit = unitService.getUnit(unitId);
        HqdProject hqdProject = projectService.getProject(year, unit.getScope());
        HqdIndex root = indexService.buildScoredIndexTree(hqdProject.getId(), unitId);
        HqdIndex index = TreeUtils.find(root, i -> i.getId().equals(indexId));
        HqdIndexExt indexExt = index.getIndexExt(unit.getAreaTag());
        if(indexExt == null) throw new IllegalStateException("本项为区域（珠三角/粤东西北）相关指标，但填报单位缺少所属区域的信息。");
        UnitScore score = index.getUnitScore();
        if(score == null || score.getStatus() == STATUS_NOT_AUDITED)
            //如果设置了填报时段，新填报及未审核填报需要符合填报时段
            assertProjectStage(hqdProject, HqdProject::getStartTime, HqdProject::getStage1Time, "填报阶段");
        else
            //被审核拒绝的填报可以无视填报时段的设置，在项目周期内重新填报
            assertProjectStage(hqdProject, HqdProject::getStartTime, HqdProject::getEndTime, "项目");
        //验证已有评分状态
        if(score != null && score.getStatus() > STATUS_NOT_AUDITED)
            throw new IllegalStateException("得分已经被审核通过，无法更改；如需更改请联系审核人员先进行审核拒绝操作。");
        //验证登入人是否具有正确的权限
        Set<String> userRoles = authService.findUserRoleKeys(subjectUser.getId());
        assertRoles(userRoles, indexExt.getReportRoles());
        //计算得分
        Float newScore;
        if(finalScore != null)
            newScore = finalScore;
        else if(rawScore != null)
            newScore = nondimensionalizeScore(rawScore, indexExt.getThresholdScore(), indexExt.getTargetScore());
        else {
            Map<String, String> scoreArgs = inputScore.getScoreArgs().stream()
                    .collect(Collectors.toMap(a -> a.getArgCode(), a -> a.getValue()));
            try {
                newScore = calculateScore(index, indexExt, scoreArgs);
                newScore = nondimensionalizeScore(newScore, indexExt.getThresholdScore(), indexExt.getTargetScore());
            } catch(Exception x) {
                log.warn("计算过程出错，返回0值", x);
                newScore = 0f;
            }
        }
        //保存得分信息
        if(score == null) {
            score = new UnitScore();
            score.setProjectId(hqdProject.getId());
            score.setUnitId(unitId);
            score.setIndexId(index.getId());
            index.setUnitScore(score);
        }
        score.setStatus(STATUS_NOT_AUDITED);
        score.setScore(0f);
        score.setUnauditedScore(newScore);
        unitService.saveScore(score);
        //保存得分填报历史
        UnitScoreUpdate scoreUpdate = new UnitScoreUpdate();
        scoreUpdate.setScoreId(score.getId());
        scoreUpdate.setStatus(score.getStatus());
        scoreUpdate.setMessage(inputScore.getUpdateMessage());
        scoreUpdate.setUpdaterId(subjectUser.getId());
        scoreUpdate.setUpdatedTime(score.getUpdatedTime());
        unitService.logScoreUpdate(scoreUpdate);
        //保存当前的得分参数数据
        Map<String, HqdIndexArg> indexArgMap = !CollectionUtils.isEmpty(index.getIndexArgs()) ?
                index.getIndexArgs().stream().collect(Collectors.toMap(HqdIndexArg::getCode, a -> a)) :
                new HashMap<>();
        Map<String, UnitScoreArg> scoreArgMap = !CollectionUtils.isEmpty(score.getScoreArgs()) ?
                score.getScoreArgs().stream().collect(Collectors.toMap(UnitScoreArg::getArgCode, a -> a)) :
                new HashMap<>();
        for(UnitScoreArg inputScoreArg : inputScore.getScoreArgs()) {
            String key = inputScoreArg.getArgCode();
            UnitScoreArg scoreArg = scoreArgMap.remove(key);
            if(scoreArg == null) {
                scoreArg = new UnitScoreArg();
                scoreArg.setScoreId(score.getId());
                scoreArg.setArgCode(key);

                HqdIndexArg indexArg = indexArgMap.get(key);
                if(indexArg == null) {
                    String placeheldKey = RegExUtils.replaceFirst(key, "[a-z]+", "@");
                    indexArg = indexArgMap.get(placeheldKey);
                }
                if(indexArg != null) {
                    scoreArg.setArgId(indexArg.getId());
                }
            }
            scoreArg.setValue(inputScoreArg.getValue());
            unitService.saveScoreArg(scoreArg);
        }
        for(Map.Entry<String, UnitScoreArg> entry : scoreArgMap.entrySet()) {
            unitService.deleteScoreArg(entry.getValue().getId());
        }
        //保存当前的得分附件数据
        Map<Long, UnitScoreAtt> scoreAttMap = !CollectionUtils.isEmpty(score.getScoreAtts()) ?
                score.getScoreAtts().stream().collect(Collectors.toMap(UnitScoreAtt::getId, a -> a)) :
                new HashMap<>();
        int seq = 0;
        for(UnitScoreAtt inputScoreAtt : inputScore.getScoreAtts()) {
            UnitScoreAtt scoreAtt = inputScoreAtt.getId() != null ? scoreAttMap.remove(inputScoreAtt.getId()) : null;
            if(scoreAtt == null) {
                scoreAtt = new UnitScoreAtt();
                scoreAtt.setScoreId(score.getId());
            }
            scoreAtt.setFilename(inputScoreAtt.getFilename());
            scoreAtt.setDownloadPath(inputScoreAtt.getDownloadPath());
            scoreAtt.setContentType(inputScoreAtt.getContentType());
            scoreAtt.setSeq(++seq);
            unitService.saveScoreAtt(scoreAtt);
        }
        for(Map.Entry<Long, UnitScoreAtt> entry : scoreAttMap.entrySet()) {
            unitService.deleteScoreAtt(entry.getValue().getId());
        }

        updateAncestorScores(index.getParent(), hqdProject.getId(), unitId);

        return newScore;
    }

    private void assertProjectStage(HqdProject hqdProject, Function<HqdProject, Date> startTimeGetter, Function<HqdProject, Date> endTimeGetter, String stageText) {
        Date now = new Date();
        Date startTime = startTimeGetter.apply(hqdProject);
        if(startTime == null) startTime = hqdProject.getStartTime();
        if(startTime != null && now.compareTo(startTime) < 0) throw new BadRequestException(stageText + "尚未开始");
        Date endTime = endTimeGetter.apply(hqdProject);
        if(endTime == null) endTime = hqdProject.getEndTime();
        if(endTime != null) {
            Calendar calendar = Calendar.getInstance();
            calendar.setTime(endTime);
            calendar.add(Calendar.DAY_OF_MONTH, 1);
            endTime = calendar.getTime();
        }
        if(endTime != null && now.compareTo(endTime) >= 0) throw new BadRequestException(stageText + "已经结束");
    }

    private void updateAncestorScores(HqdIndex index, long projectId, long unitId) {
        HqdIndex parent = index;
        while(parent != null) {
            Tuple3<Float, Float, Boolean> tuple = accumulateScore(parent);

            UnitScore score = parent.getUnitScore();
            if(score == null) {
                score = new UnitScore();
                score.setProjectId(projectId);
                score.setUnitId(unitId);
                score.setIndexId(parent.getId());
                parent.setUnitScore(score);
            }
            Integer status = tuple._3() ? STATUS_APPROVED : STATUS_NOT_AUDITED;
            if(!tuple._1().equals(score.getScore()) || !tuple._2().equals(score.getUnauditedScore()) || !status.equals(score.getStatus())) {
                score.setScore(tuple._1());
                score.setUnauditedScore(tuple._2());
                score.setStatus(status);
                score.setUpdatedTime(new Date());
                unitService.saveScore(score);
            }
            parent = parent.getParent();
        }
    }

    private static Tuple3<Float, Float, Boolean> accumulateScore(HqdIndex index) {
        float weightTotal = 0, scoreTotal = 0, unauditedScoreTotal = 0;
        boolean audited = true;
        for(HqdIndex child : index.getChildren()) {
            UnitScore childScore = child.getUnitScore();
            if(child.getWeight() == null) child.setWeight(100f / index.getChildren().size());
            weightTotal += child.getWeight();
            if(childScore != null) {
                scoreTotal += childScore.getScore() * child.getWeight();
                unauditedScoreTotal += childScore.getUnauditedScore() * child.getWeight();
                audited &= childScore.getStatus() == STATUS_APPROVED;
            }
        }
        return Tuple.of(scoreTotal / weightTotal, unauditedScoreTotal / weightTotal, audited);
    }

    @ApiOperation("刷新一、二级指标项得分（重新计算本节点和祖先节点的得分得分并更新）")
    @PostMapping("/{year}/units/{unitId}/index-scores/{indexId}/refresh")
    @RequiresRoles(ROLE_REPORTER)
    @Traced
    public Object indexScoresRefresh(@ApiParam("年份") @PathVariable("year") Integer year,
                                     @ApiParam("单位id") @PathVariable("unitId") Long unitId,
                                     @ApiParam("指标项id") @PathVariable("indexId") Long indexId,
                                     @ApiIgnore User subjectUser,
                                     @ApiIgnore List<Long> authorizedDomainIds) {
        assertAuthorizedUnit(unitId, authorizedDomainIds);
        Unit unit = unitService.getUnit(unitId);
        HqdProject hqdProject = projectService.getProject(year, unit.getScope());
        assertProjectStage(hqdProject, HqdProject::getStartTime, HqdProject::getEndTime, "项目");
        HqdIndex root = indexService.buildScoredIndexTree(hqdProject.getId(), unitId);
        HqdIndex index = TreeUtils.find(root, i -> i.getId().equals(indexId));
        updateAncestorScores(index, hqdProject.getId(), unitId);
        return index.getUnitScore().getScore();
    }

    @ApiOperation("获取得分变更（填报、审核）历史")
    @GetMapping("/{year}/units/{unitId}/index-scores/{indexId}/updates")
    @RequiresAuthentication
    public Object indexScoresUpdates(@ApiParam("年份") @PathVariable("year") Integer year,
                                     @ApiParam("单位id") @PathVariable("unitId") Long unitId,
                                     @ApiParam("指标项id") @PathVariable("indexId") Long indexId,
                                     @ApiParam("得分id") @RequestParam(value = "scoreId", required = false) Long scoreId) {
        if(scoreId == null) {
            Unit unit = unitService.getUnit(unitId);
            HqdProject hqdProject = projectService.getProject(year, unit.getScope());
            UnitScore score = unitService.getScore(hqdProject.getId(), unitId, indexId);
            if(score != null) scoreId = score.getId();
        }
        return scoreId != null ? unitService.getScoreUpdates(scoreId) : new ArrayList<>();
    }

    @ApiOperation("审核指标项得分")
    @PostMapping("/{year}/units/{unitId}/index-scores/{indexId}/audit")
    @RequiresRoles(ROLE_AUDITOR)
    @Traced
    public Object indexScoresAudit(@ApiParam("年份") @PathVariable("year") Integer year,
                                   @ApiParam("单位id") @PathVariable("unitId") Long unitId,
                                   @ApiParam("指标项id") @PathVariable("indexId") Long indexId,
                                   @ApiParam("审核赋分（仅强制赋分时使用，其余情况不要传此参数）") @RequestParam(value = "finalScore", required = false) Float finalScore,
                                   @ApiParam("审核状态：1-审核通过，-1=审核驳回") @RequestParam("auditStatus") Integer auditStatus,
                                   @ApiParam("审核意见") @RequestParam(value = "auditMessage", required = false) String auditMessage,
                                   @ApiIgnore User subjectUser,
                                   @ApiIgnore List<Long> authorizedDomainIds) {
        assertAuthorizedUnit(unitId, authorizedDomainIds);
        Unit unit = unitService.getUnit(unitId);
        HqdProject hqdProject = projectService.getProject(year, unit.getScope());
        assertProjectStage(hqdProject, HqdProject::getStartTime, HqdProject::getStage2Time, "审核阶段");
        HqdIndex root = indexService.buildScoredIndexTree(hqdProject.getId(), unitId);
        HqdIndex index = TreeUtils.find(root, i -> i.getId().equals(indexId));
        HqdIndexExt indexExt = index.getIndexExt(unit.getAreaTag());
        //登入人是否具有正确的审核权限
        UnitScore score = index.getUnitScore();
        if(score == null) throw new IllegalStateException("评分未填报，非法访问。");
        String[] auditorRoles = indexExt.getAuditorRoles();
        Set<String> userRoles = authService.findUserRoleKeys(subjectUser.getId());
        int auditStep = 0;
        if(score.getStatus() == STATUS_NOT_AUDITED) {
            //下一级：初审
            if(userRoles.contains(auditorRoles[0]))
                auditStep = 1;
            else
                throw new AuthorizationException("需要角色：" + auditorRoles[0]);
        } else switch(auditorRoles.length) {
            case 3:
                if(Math.abs(score.getStatus()) == STATUS_APPROVED3) {
                    //下一级：复审
                    if(score.getStatus() > 0 && userRoles.contains(auditorRoles[1]))
                        auditStep = 2;
                    //本级：初审
                    else if(userRoles.contains(auditorRoles[0]))
                        auditStep = 1;
                    else
                        throw new AuthorizationException("需要角色：" + auditorRoles[1] + "/" + auditorRoles[0]);
                } else if(Math.abs(score.getStatus()) == STATUS_APPROVED2) {
                    //下一级：终审
                    if(score.getStatus() > 0 && userRoles.contains(auditorRoles[2]))
                        auditStep = 3;
                    //本级：复审
                    else if(userRoles.contains(auditorRoles[1]))
                        auditStep = 2;
                    else
                        throw new AuthorizationException("需要角色：" + auditorRoles[2] + "/" + auditorRoles[1]);
                } else if(Math.abs(score.getStatus()) == STATUS_APPROVED) {
                    //本级：终审
                    if(userRoles.contains(auditorRoles[2]))
                        auditStep = 3;
                    else
                        throw new AuthorizationException("需要角色：" + auditorRoles[2]);
                }
                break;
            case 2:
                if(Math.abs(score.getStatus()) == STATUS_APPROVED2) {
                    //下一级：终审
                    if(score.getStatus() > 0 && userRoles.contains(auditorRoles[1]))
                        auditStep = 2;
                    //本级：初审
                    else if(userRoles.contains(auditorRoles[0]))
                        auditStep = 1;
                    else
                        throw new AuthorizationException("需要角色：" + auditorRoles[1] + "/" + auditorRoles[0]);
                } else if(Math.abs(score.getStatus()) == STATUS_APPROVED) {
                    //本级：终审
                    if(userRoles.contains(auditorRoles[1]))
                        auditStep = 2;
                    else
                        throw new AuthorizationException("需要角色：" + auditorRoles[1]);
                }
                break;
        }
        //保存得分信息
        score.setStatus(auditStatus * (auditorRoles.length - auditStep + 1));
        if(finalScore != null) score.setUnauditedScore(finalScore);
        score.setScore(score.getStatus() == STATUS_APPROVED ? score.getUnauditedScore() : 0f);
        unitService.saveScore(score);
        //保存得分审核历史
        UnitScoreUpdate scoreUpdate = new UnitScoreUpdate();
        scoreUpdate.setScoreId(score.getId());
        scoreUpdate.setStatus(auditStatus);
        scoreUpdate.setMessage(auditMessage);
        scoreUpdate.setUpdaterId(subjectUser.getId());
        scoreUpdate.setUpdatedTime(score.getUpdatedTime());
        unitService.logScoreUpdate(scoreUpdate);

        updateAncestorScores(index.getParent(), hqdProject.getId(), unitId);

        return auditStatus;
    }

    private void assertRole(Set<String> userRoles, String role, String message) {
        if(!userRoles.contains(role)) {
            throw new AuthorizationException(StringUtils.hasText(message) ? message + role : "需要角色：" + role);
        }
    }

    @ApiOperation("导出单位的指标树（含当前得分）")
    @GetMapping(value = "/{year}/units/{unitId}/index-scores/export")
    @RequiresRoles(ROLE_STATISTICIAN)
    public void exportIndexScores(@ApiParam("年份") @PathVariable("year") Integer year,
                                  @ApiParam("单位id") @PathVariable("unitId") Long unitId,
                                  @ApiParam("层数") @RequestParam(value = "maxLevel", defaultValue = "3") int maxLevel,
                                  @ApiIgnore HttpServletResponse response) {
        Unit unit = unitService.getUnit(unitId);
        HqdProject hqdProject = getProject(year, unit.getScope());
        UnitScoreTableExcel unitScoreTableExcel = new UnitScoreTableExcel(maxLevel);
        HqdIndex root = indexService.buildScoredIndexTree(hqdProject.getId(), unit.getId());
        try {
            response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
            String filename = hqdProject.getYear() + unit.getShortName() + ".xlsx";
            String encodedFilename = URLEncoder.encode(filename, StandardCharsets.UTF_8.toString());
            response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + encodedFilename);

            OutputStream out = response.getOutputStream();
            unitScoreTableExcel.render(root, out);
            out.flush();
        } catch(Exception x) {
            x.printStackTrace();
        }
    }

    private HqdProject getProject(int year, int scope) {
        HqdProject prj = projectService.getProject(year, scope);
        if(prj == null) throw new BadRequestException("项目未找到（" + year + "年" + Unit.SCOPE_TEXTS.get(scope) + "），请确保项目已创建");
        return prj;
    }

    @ApiOperation("批量更改得分")
    @PostMapping(value = "/batch/indexes/{indexId}/update")
    @RequiresRoles(ROLE_ADMINISTRATOR)
    public Object batchUpdateScore(@ApiParam("指标id") @PathVariable("indexId") Long indexId,
                                   @ApiParam("项目Id") @RequestParam(value = "projectId", required = false) Long projectId,
                                   @ApiParam("单位id") @RequestParam(value = "unitId", required = false) Long unitId,
                                   @ApiParam("指标得分（新）") @RequestParam(value = "newScore", required = false) Float newScore,
                                   @ApiParam("指标未审核得分（新）") @RequestParam(value = "newUnauditedScore", required = false) Float newUnauditedScore,
                                   @ApiParam("指标状态（新）") @RequestParam(value = "newStatus", required = false) Integer newStatus,
                                   @ApiParam("操作备注") @RequestParam(value = "message", defaultValue = "更改评分（管理员批量操作）") String message,
                                   @ApiIgnore User subjectUser) {
        if(newScore == null && newUnauditedScore == null && newStatus == null) return null;
        return batchOp(indexId, projectId, unitId, (unit, index) -> {
            UnitScore unitScore = index.getUnitScore();
            if(unitScore == null) return false;
            if(newScore != null) unitScore.setScore(newScore);
            if(newUnauditedScore != null) unitScore.setUnauditedScore(newUnauditedScore);
            if(newStatus != null) {
                unitScore.setStatus(newStatus);
                if(newStatus < 0) unitScore.setScore(0f);
            }
            unitService.saveScore(unitScore);
            return true;
        }, message, subjectUser);
    }

    private Integer batchOp(Long indexId, Long projectId, Long unitId, BiFunction<Unit, HqdIndex, Boolean> op, String message, User subjectUser) {
        HqdIndex index0 = indexService.getIndex(indexId);
        if(index0 == null) return null;

        List<HqdProject> projects;
        if(projectId != null) {
            projects = new ArrayList<>();
            HqdProject project = projectService.getProject(projectId);
            projects.add(project);
        } else {
            projects = projectService.getActiveProjects(new Date(), null).stream()
                    .filter(project -> project.getIndexVerId().equals(index0.getVerId()))
                    .collect(Collectors.toList());
        }

        int n = 0;
        for(HqdProject hqdProject : projects) {
            List<Unit> units;
            if(unitId != null) {
                units = new ArrayList<>();
                Unit unit = unitService.getUnit(unitId);
                if(unit != null) units.add(unit);
            } else {
                units = unitService.getUnits(hqdProject.getScope());
            }
            if(CollectionUtils.isEmpty(units)) return null;

            for(Unit unit : units) {
                HqdIndex root = indexService.buildScoredIndexTree(hqdProject.getId(), unit.getId());
                HqdIndex index = TreeUtils.find(root, i -> i.getId().equals(indexId));
                if(op.apply(unit, index)) {
                    UnitScore unitScore = index.getUnitScore();
                    UnitScoreUpdate scoreUpdate = new UnitScoreUpdate();
                    scoreUpdate.setScoreId(unitScore.getId());
                    scoreUpdate.setStatus(unitScore.getStatus());
                    scoreUpdate.setMessage(message);
                    scoreUpdate.setUpdaterId(subjectUser.getId());
                    scoreUpdate.setUpdatedTime(unitScore.getUpdatedTime());
                    unitService.logScoreUpdate(scoreUpdate);
                    updateAncestorScores(index.getParent(), hqdProject.getId(), unit.getId());
                    n++;
                }
            }
        }
        return n;
    }

    @ApiOperation("批量重新计算得分")
    @PostMapping(value = "/batch/indexes/{indexId}/recalc")
    @RequiresRoles(ROLE_ADMINISTRATOR)
    public Object batchRecalcScore(@ApiParam("指标id") @PathVariable("indexId") Long indexId,
                                   @ApiParam("项目Id") @RequestParam(value = "projectId", required = false) Long projectId,
                                   @ApiParam("单位id") @RequestParam(value = "unitId", required = false) Long unitId,
                                   @ApiParam("操作备注") @RequestParam(value = "message",defaultValue = "重新计算（管理员批量操作）") String message,
                                   @ApiIgnore User subjectUser) {
        return batchOp(indexId, projectId, unitId, (unit, index) -> {
            UnitScore unitScore = index.getUnitScore();
            if(unitScore == null) return false;
            HqdIndexExt indexExt = index.getIndexExt(unit.getAreaTag());
            if(indexExt == null) {
                log.warn("本项为区域（珠三角/粤东西北）相关指标，但填报单位缺少所属区域的信息。");
                return false;
            }
            Map<String, String> scoreArgs = unitScore.getScoreArgs().stream()
                    .collect(Collectors.toMap(a -> a.getArgCode(), a -> a.getValue()));
            Float newScore;
            try {
                newScore = calculateScore(index, indexExt, scoreArgs);
                newScore = nondimensionalizeScore(newScore, indexExt.getThresholdScore(), indexExt.getTargetScore());
                unitScore.setScore(newScore);
            } catch(Exception x) {
                log.warn("计算过程出错，置为终审拒绝", x);
                unitScore.setScore(0f);
                unitScore.setStatus(-STATUS_APPROVED);
            }
            unitService.saveScore(unitScore);
            return true;
        }, message, subjectUser);
    }

}
